
Alkaline.Modules.newModule('Optimize', function() {

this.requiredModules = [{ name: 'Analyzer' }];

var DEF_O = 1, MAX_O = 2;
this.registerSupportedArgument('O', 'O', 1, DEF_O);
for (var i = 0; i < MAX_O; i++)
	this.registerSupportedArgument('O' + i, 'O' + i, 0, false);

var TOKEN = Alkaline.JSInfo.TOKEN;
var tokenNames = Alkaline.JSInfo.tokenNames;

var ProcessingState = Alkaline.Processing.ProcessingState;
var LexicalContext = Alkaline.Processing.LexicalContext;
var VariableDefinition = Alkaline.Processing.VariableDefinition;
var ReferencedValue = Alkaline.Processing.ReferencedValue;
var FunctionInstance = Alkaline.Processing.FunctionInstance;
var ValueNull = Alkaline.Processing.ValueNull;
var ValueUndefined = Alkaline.Processing.ValueUndefined;
var Operation = Alkaline.Processing.Operation;
var Condition = Alkaline.Processing.Condition;
var ValueDistribution = Alkaline.Processing.ValueDistribution;
var ValueRegExp = Alkaline.Processing.ValueRegExp;

/**
 *	run ( ast )
 */
this.run = function(ast) {
	this._setup();
	this.taskQueue.push(ast);
	var time = new Date().valueOf();
	this.doTasks();
	time = new Date().valueOf() - time;
	this.compileResult.stdOut("[Optimize] " + this.ops + " transformations have been performed over " + this.taskCount + " tasks in " + time + " ms.");
}

this._setup = function() {
	this.builder = this.compileResult.treeBuilder;
	this.taskQueue = [];
	this.taskCount = 0;
	this.ops = 0;
	if (!this.arguments.hasOwnProperty('O') && this.arguments.O0)
		this.arguments.O = 0;
	for (var i = 1; i < MAX_O; i++)
		if (this.arguments['O' + i] && this.arguments.O < i)
			this.arguments.O = i;
	for (var i = 0; i < MAX_O; i++)
		this.arguments['O' + i] = this.arguments.O >= i;
}

this.doTasks = function() {
	var i = 0;
	var taskBatch = 200;
	while (i < this.taskQueue.length) {
		var task = this.taskQueue[i];
		i++;
		if (i === taskBatch) {
			this.taskQueue.splice(0, taskBatch);
			i = 0;
		}
		this.doTask(task);
	}
}

this.doTask = function(task) {
	if (!task.doneOnce) {
		task.doneOnce = true;
		this.taskCount++;
	}
	
	do {
		var ops = this.ops;
		
		if (task.token.type === TOKEN.BLOCK) {
			this.state = new ProcessingState(this.builder, task);
			this.state.currentContext.resetLocals();
			this.process(task);
		} else if (task.token.type === TOKEN.Function) {
			var BLOCK = task.children[task.children.length-1];
			this.state = new ProcessingState(this.builder, BLOCK);
			this.state.currentContext.resetLocals();
			this.process(BLOCK);
		} else
			this.compileResult.stdWarn('[Optimize] Processing tasks of type ' + task.token.type + ' is not (yet) implemented.');
		
		this.state = null;
	} while (this.ops > ops);	// keep optimizing until we run out of moves
}

this.getReference = function(ast, fromGetRef) {
	if (ast.token.type === TOKEN.Id)
		return ast.objectValue;
	if (fromGetRef)
		return this.process(ast);
	if (ast.token.type === TOKEN.SELECT) {
		var base = this.getReference(ast.children[0], true).getValue(this.state.condition);
		var selector = this.process(ast.children[1]);
		var selectorVal = null;
		if (selector.values.length === 1)
			switch (selector.valueType) {
				
				case ReferencedValue.VALUE_TYPE.STRING:
					if (selector.value !== null)
						selectorVal = Alkaline.JSInfo.unescapeString(selector.value);
					break;
				
				case ReferencedValue.VALUE_TYPE.NUMBER:
					if (selector.value !== null)
						selectorVal = selector.value+'';
					break;
				
				case ReferencedValue.VALUE_TYPE.BOOLEAN:
					if (selector.value !== null)
						selectorVal = selector.value+'';
					break;
				
				case ReferencedValue.VALUE_TYPE.NULL:
					selectorVal = 'null';
					break;
				
				case ReferencedValue.VALUE_TYPE.UNDEFINED:
					selectorVal = 'undefined';
					break;
			}
		return base.getPropertyDescriptor(this.state.condition, selectorVal);
	}
	return null;
}

this.tryMergeStatements = function(condition, statement1, statement2) {
	var iExpr1 = statement1.children;
	var iExpr2 = statement2.children;
	var args = [];
	if (iExpr1 && iExpr1.length > 1 || iExpr2 && iExpr2.length > 1) {
		if (!iExpr1 || !iExpr2 || iExpr1.length !== iExpr2.length)
			return null;
		for (var i = 0; i < iExpr1.length-1; i++)
			args.push(iExpr1[i]);
	}
	iExpr1 = iExpr1.last() || this.builder.EXPR();
	iExpr2 = iExpr2.last() || this.builder.EXPR();
	args.push(this.builder.QMark(condition, iExpr1, iExpr2));
	var newStmt = this.builder[statement1.token.type].apply(this.builder, args);
	return newStmt;
}

this.poisonLocals = function(ast) {
	// ast may be a list of ASTs
	// raw implementation, but err on the safe side for now
	var locals = this.state.currentContext.getLocalVariables();
	var cond = new Condition(this.state.condition);
	for (var i = 0; i < locals.length; i++)
		locals[i].setValue(cond, new ReferencedValue(ReferencedValue.VALUE_TYPE.UNKNOWN, {}));
}

this.unpoisonLocals = function() {
	
}

/**
 *	AST processing
 */
this.process = function(ast) {
	return this['process' + ast.token.type](ast);
}

this.processARRAY = function(ast) {
	var arr = new ReferencedValue(ReferencedValue.VALUE_TYPE.ARRAY, []);
	arr.ast = ast;
	if (ast.children && ast.children.length)
		for (var i = 0; i < ast.children.length; i++)
			arr.value.push(this.process(ast.children[i]));
	arr.getPropertyDescriptor(Condition.ALWAYS, 'length').setValue(Condition.ALWAYS, new ReferencedValue(ReferencedValue.VALUE_TYPE.NUMBER, arr.value.length));
	return new ValueDistribution({ condition: this.state.condition, value: arr });
}

this.processBLOCK = function(ast) {
	if (ast.parent && ast.parent.token.type === TOKEN.BLOCK && ast.children && ast.children.length) {
		var children = this.builder.nil();
		children.children = ast.children;
		ast.children = [];
		ast.parent.replaceChildren(ast.childIndex, ast.childIndex, children);
		this.ops++;
		return;
	}
	if (ast.children) {
		var oldChildCount = ast.children.length;
		for (var i = 0; i < ast.children.length; i++)
			if (ast.children[i].token.type === TOKEN.BLOCK && (!ast.children[i].children || !ast.children[i].children.length)) {
				ast.deleteChild(i);
				this.ops++;
				i--;
			} else {
				// merge consecutive expressions into one:
				// a(); b(); -> a(), b();
				// do not merge first strings - they may have special meaning like "use strict" and if they don't, they will be removed anyway
				if (ast.children[i].token.type !== TOKEN.String && Alkaline.JSInfo.isExpression(ast.children[i])) {
					var j = i+1;
					while (j < ast.children.length && Alkaline.JSInfo.isExpression(ast.children[j]))
						j++;
					if (i < j-1) {
						ast.replaceChildren(i, j-1, this.builder.EXPR(ast.children.slice(i, j)));
						this.ops++;
					}
				}
				this.process(ast.children[i]);
			}
		if (ast.children.length !== oldChildCount && ast.parent) {
			this.compileResult._doneWithTree(ast);	// maybe the block can be eliminated
			if (ast.parent.children[ast.childIndex] !== ast)
				this.ops++;	// this block has been eliminated
		}
	}
}

this.processBreak = function(ast) {
	// TODO: handle Break
}

this.processCALL = function(ast) {
	var func = this.process(ast.children[0]);
	for (var i = 1; i < ast.children.length; i++)
		this.process(ast.children[i]);
	var value = new ReferencedValue(ReferencedValue.VALUE_TYPE.UNKNOWN);	// we don't know what this will return... yet
	return new ValueDistribution([{ condition: this.state.condition, value: value }]);
}

this.processCase = function(ast) {
	this.process(ast.children[0]);
	this.state.pushCondition(new Condition(this.state.condition));
	this.process(ast.children[1]);
	this.state.popCondition();
}

this.processCatch = function(ast) {
	// TODO: manage the exception variable and mark the state
	// TODO: handle conditional catch
	this.state.pushCondition(new Condition(this.state.condition));
	this.poisonLocals();
	this.process(ast.children[ast.children.length-1]);
	this.unpoisonLocals();
	this.state.popCondition();
}

this.processComment = function(ast) {
	// nothing to do... yet
}

this.processContinue = function(ast) {
	// TODO: handle Continue
}

this.processDefault = function(ast) {
	this.state.pushCondition(new Condition(this.state.condition));
	this.process(ast.children[0]);
	this.state.popCondition();
}

this.processDefaultDirective = function(ast) {
	// [0] = "xml"
	// [1] = "namespace"
	this.process(ast.children[2]);
}

this.processDelete = function(ast) {
	// not yet implemented
	return new ValueDistribution({
		condition: this.state.condition,
		value: new ReferencedValue(ReferencedValue.VALUE_TYPE.BOOLEAN, null)
	});
}

this.processDo = function(ast) {
	ast.children[1].coercedTo = ReferencedValue.VALUE_TYPE.BOOLEAN;
	ast.children[1].coercedBy = ast;
	this.poisonLocals(ast);
	this.process(ast.children[0]);
	var cond = this.process(ast.children[1]);
	this.unpoisonLocals();
	if (this.getTruthiness(cond) === false) {
		this.builder.replaceTree(ast, this.builder.BLOCK(ast.children));
		this.ops++;
		return;
	}
}

this.processEMPTY = function(ast) {
	// nothing to do
}

this.processEXPR = function(ast) {
	var lastChild, toReturn, newChildren;
	for (var i = 0; i < ast.children.length; i++) {
		var lastChild = this.process(ast.children[i]);
		if (ast.children[i].token.type === TOKEN.BLOCK && (!ast.children[i].children || !ast.children[i].children.length)) {
			this.ops++;
			ast.deleteChild(i);
			i--;	
		} else {
			toReturn = lastChild;
			if (ast.children[i].token.type === TOKEN.EXPR) {
				newChildren = ast.children[i].children || [];
				if (newChildren.length) {
					this.builder.setChild(ast, i, newChildren[0]);
					for (var j = 1; j < newChildren.length; j++)
						this.builder.addChild(ast, newChildren[j], i + j);
				} else
					ast.deleteChild(i);
				this.ops++;
				i += newChildren.length-1;
			}
		}
	}
	if (ast.children.length === 1) {
		this.builder.replaceTree(ast, ast.children[0]);
		this.ops++;
	} else if (!ast.children.length) {
		this.builder.replaceTree(ast, this.builder.BLOCK());
		this.ops++;
	}
	return toReturn;
}

this.processFILTER = function(ast) {
	if (!Alkaline.JSInfo.returnValueIsUsed(ast)) {
		this.builder.replaceTree(ast, ast.children[0]);
		this.ops++;
		return;
	}
	this.process(ast.children[0]);
	this.process(ast.children[1]);
	var value = new ReferencedValue(ReferencedValue.VALUE_TYPE.UNKNOWN_NATIVE);
	return new ValueDistribution([{ condition: this.state.condition, value: value }]);
}

this.processFinally = function(ast) {
	this.process(ast.children[0]);
}

this.processFor = function(ast) {
	if (ast.children.length < 4) {
		// for-in or for-each-in
		var each = ast.children[0].token.type === TOKEN.Each;
		this.process(ast.children[0 + each].children[1]);
		this.state.pushCondition(new Condition(this.state.condition));
		this.poisonLocals(ast.children[1 + each]);
		this.process(ast.children[1 + each]);
		this.unpoisonLocals();
		this.state.popCondition();
	} else {
		// classic for
		ast.children[1].coercedTo = ReferencedValue.VALUE_TYPE.BOOLEAN;
		ast.children[1].coercedBy = ast;
		this.process(ast.children[0]);
		
		var cond = this.process(ast.children[1]);
		if (this.getTruthiness(cond) === false) {
			this.builder.replaceTree(ast, ast.children[0].token.type === TOKEN.EMPTY ? this.builder.BLOCK([]) : ast.children[0]);
			this.ops++;
			return;
		}
		
		this.state.pushCondition(new Condition(this.state.condition));
		this.poisonLocals([ast.children[3], ast.children[2]]);
		this.process(ast.children[3]);
		this.process(ast.children[2]);
		this.unpoisonLocals();
		this.state.popCondition();
		if (ast.childIndex && ast.parent.token.type === TOKEN.BLOCK && Alkaline.JSInfo.isExpression(ast.parent.children[ast.childIndex-1]))
			this.builder.setChild(ast, 0, this.builder.EXPR([this.builder.removeTree(ast.parent.children[ast.childIndex-1]), ast.children[0]]));
		if (Alkaline.JSInfo.isExpression(ast.children[3])) {
			this.builder.setChild(ast, 2, this.builder.EXPR([this.builder.removeTree(ast.children[3]), ast.children[2]]));
			this.builder.addChild(ast, this.builder.BLOCK());
		}
	}
}

this.processFunction = function(ast) {
	if (!ast.optimized) {
		// make sure we only add this to the list of tasks once
		this.taskQueue.push(ast);
		ast.optimized = true;
	}
	return new ValueDistribution({ condition: this.state.condition, value: ast.objectValue });
}

this.processId = function(ast) {
	if (!Alkaline.JSInfo.returnValueIsUsed(ast)) {
		this.builder.replaceTree(ast, this.builder.BLOCK());
		this.ops++;
		return;
	}
	return ast.objectValue.getValue(this.state.condition);
}

this.processIf = function(ast) {
	var hasElse = ast.children.length === 3;
	if (hasElse) {
		if (Alkaline.JSInfo.isExpression(ast.children[1]) && Alkaline.JSInfo.isExpression(ast.children[2])) {
			var newOp;
			this.builder.replaceTree(ast, newOp = this.builder.QMark(ast.children[0], ast.children[1], ast.children[2]));
			this.ops++;
			return this.process(newOp);
		}
		if (ast.children[2].token.type === TOKEN.BLOCK && (!ast.children[2].children || !ast.children[2].children.length)) {
			this.builder.removeTree(ast.children[2]);
			hasElse = false;
			this.ops++;
		}
	}
	if (hasElse && ast.children[1].token.type === ast.children[2].token.type && Alkaline.JSInfo.isStatementExpression(ast.children[1])) {
		// if (X) return A; else return B; => return X ? A : B;
		// also for Throw and DefaultDirective
		var newStmt = this.tryMergeStatements(ast.children[0], ast.children[1], ast.children[2]);
		if (newStmt) {
			this.builder.replaceTree(ast, newStmt);
			this.ops++;
			this.process(newStmt);
			return;
		}
	}
	if (ast.children[1].token.type === TOKEN.BLOCK && (!ast.children[1].children || !ast.children[1].children.length))
		// if (X) {} [else Y];
		if (hasElse) {
			// if (X) {} else Y; --> if (!X) Y;
			this.builder.setChild(ast, 0, this.builder.UnaryOperator('!', ast.children[0]));
			ast.deleteChild(1);
			hasElse = false;
			this.ops++;
		} else {
			// if (X) {}; --> X;
			this.builder.replaceTree(ast, ast.children[0]);
			this.process(ast.children[0]);
			this.ops++;
			return;
		}
	if (!hasElse && ast.children[1].token.type === TOKEN.If && ast.children[1].children.length === 2) {
		// if (X) if (Y) Z; --> if (X && Y) Z;
		var child = ast.children[1];
		var cond = ast.children[0];
		do {
			cond = this.builder.Operator('&&', cond, child.children[0]);
			child = child.children[1];
		} while (child.token.type === TOKEN.If && child.children.length === 2);
		ast.replaceChildren(0, 0, cond);
		ast.replaceChildren(1, 1, child);
	}
	if (!hasElse && Alkaline.JSInfo.isExpression(ast.children[1])) {
		// if (X) Y; --> X && Y;
		var newOp;
		this.builder.replaceTree(ast, newOp = this.builder.Operator('&&', ast.children[0], ast.children[1]));
		this.ops++;
		return this.process(newOp);
	}
	if (!hasElse && Alkaline.JSInfo.isStatementExpression(ast.children[1]) && ast.parent.token.type === TOKEN.BLOCK && ast.parent.children.length > ast.childIndex+1 &&
		ast.children[1].token.type === ast.parent.children[ast.childIndex+1].token.type) {
		// if (X) return Y; return Z; --> return X ? Y : Z;
		// also for Throw and DefaultDirective
		var newStmt = this.tryMergeStatements(ast.children[0], ast.children[1], ast.parent.children[ast.childIndex+1]);
		if (newStmt) {
			this.builder.replaceChildren(ast.parent, ast.childIndex, ast.childIndex+1, newStmt);
			this.ops++;
			this.process(newStmt);
			return;
		}
	}
	ast.children[0].coercedTo = ReferencedValue.VALUE_TYPE.BOOLEAN;
	ast.children[0].coercedBy = ast;
	this.process(ast.children[0]);
	this.state.pushCondition(new Condition(this.state.condition));
	this.process(ast.children[1]);
	this.state.popCondition();
	if (hasElse) {
		this.state.pushCondition(new Condition(this.state.condition));
		this.process(ast.children[2]);
		this.state.popCondition();
	}
}

this.processLABEL = function(ast) {
	// TODO: remember label
	this.process(ast.children[1]);
	// TODO: remove label from memory
}

this.processNew = function(ast) {
	var clss = this.process(ast.children[0]);
	var params = [];
	for (var i = 1; i < ast.children.length; i++)
		params.push(this.process(ast.children[i]));
	var value = new ReferencedValue(ReferencedValue.VALUE_TYPE.UNKNOWN);
	return new ValueDistribution([{ condition: this.state.condition, value: value }]);
}

this.processOBJECT = function(ast) {
	var obj = new ReferencedValue(ReferencedValue.VALUE_TYPE.OBJECT, {});
	obj.ast = ast;
	if (ast.children && ast.children.length)
		for (var i = 0; i < ast.children.length; i++) {
			var child = ast.children[i];
			if (ast.children[i].token.type === TOKEN.String)
				this.process(child.children[0]);
			else if (!child.optimized) {
				this.taskQueue.push(child);
				child.optimized = true;
			}
		}
	return new ValueDistribution({ condition: this.state.condition, value: obj });
}

this.processOperator = function(ast) {
	if (ast.token.text === "?" && this.builder.tokensMatch(ast.children[1], ast.children[2])) {
		var fullMatch = true, partialMatch;
		if (ast.children[1].children && ast.children[1].children.length)
			if (ast.children[1].length !== ast.children[2].length)
				fullMatch = false;
			else
				for (var i = 0; i < ast.children[1].children.length; i++)
					if (!this.builder.treesMatch(ast.children[1].children[i], ast.children[2].children[i]))
						if (i && partialMatch == null) {
							// if we passed 0 (and the branches only diverge in one point) then we have a partial match
							partialMatch = i;
							fullMatch = false;
						} else {
							// otherwise, if the branches diverge in any point we don't have any sort of match
							fullMatch = false;
							partialMatch = null;
							break;
						}
			
		if (fullMatch) {
			var result = this.builder.EXPR([ ast.children[0], ast.children[1] ]);
			this.builder.replaceTree(ast, result);
			this.ops++;
			return this.process(result);
		} else if (partialMatch != null) {
			var result = ast.children[1];
			ast.parent.setChild(ast.childIndex, result);
			ast.setChild(1, result.children[partialMatch]);
			ast.setChild(2, ast.children[2].children[partialMatch]);
			result.setChild(partialMatch, ast);
			this.ops++;
			return this.process(result);
		}
	}
	if (ast.token.text === "?") {
		//
		// ternary operator
		//
		ast.children[0].coercedTo = ReferencedValue.VALUE_TYPE.BOOLEAN;
		ast.children[0].coercedBy = ast;
		var condBranch = this.process(ast.children[0]);
		
		var knownBranch = this.getTruthiness(condBranch);
		if (knownBranch !== null) {
			// keep only one branch
			var result = knownBranch ? ast.children[1] : (ast.children[2] || this.builder.BLOCK());
			this.builder.replaceTree(ast, this.builder.EXPR([ast.children[0], result]));
			this.ops++;
			return this.process(result);
		}

		if (ast.children[0].token.text === "!" && ast.children[0].token.type === TOKEN.UnaryOperator) {
			// !a ? b : c --> a ? c : b;
			ast.replaceChildren(0, 0, ast.children[0].children[0]);
			var oldBranch = ast.children[1];
			ast.replaceChildren(1, 1, ast.children[2]);
			ast.replaceChildren(2, 2, oldBranch);
			ast.children[0].coercedTo = ReferencedValue.VALUE_TYPE.BOOLEAN;
			ast.children[0].coercedBy = ast;
			this.ops++;
			condBranch = this.process(ast.children[0]);
		}
		this.state.pushCondition(new Condition(this.state.condition));
		var trueBranch = this.process(ast.children[1]);
		this.state.popCondition();
		this.state.pushCondition(new Condition(this.state.condition));
		var falseBranch = this.process(ast.children[2]);
		this.state.popCondition();
		
		// if the return value does not matter and one of the sides is empty, simplify
		if (!trueBranch) {
			this.builder.replaceTree(ast,
				!falseBranch ?
					ast.children[0] :
					this.builder.Operator('&&', this.builder.UnaryOperator('!', ast.children[0]), ast.children[2])
			);
			this.ops++;
			return;
		} else if (!falseBranch) {
			this.builder.replaceTree(ast, this.builder.Operator('&&', ast.children[0], ast.children[1]));
			this.ops++;
			return;
		}
		
		var value;
		// check for: x ? true : false
		if (trueBranch.values.length === 1 && trueBranch.values[0] === ReferencedValue.primitives.True) {
			if (falseBranch.values.length === 1 && falseBranch.values[0] === ReferencedValue.primitives.False)
				if (condBranch.values.length === 1 && condBranch.values[0].valueType === ReferencedValue.VALUE_TYPE.BOOLEAN) {
					this.builder.replaceTree(ast, ast.children[0]);
					this.ops++;
					value = condBranch;
				} else {
					this.builder.replaceTree(ast, this.builder.UnaryOperator('!', this.builder.UnaryOperator('!', ast.children[0])));
					this.ops++;
				}
		}
		// check for: x ? false : true
		if (trueBranch.values.length === 1 && trueBranch.values[0] === ReferencedValue.primitives.False) {
			if (falseBranch.values.length === 1 && falseBranch.values[0] === ReferencedValue.primitives.True) {
				this.builder.replaceTree(ast, this.builder.UnaryOperator('!', ast.children[0]));
				this.ops++;
			}
		}
		if (!value) {
			var values = trueBranch.values.concat(falseBranch.values);
			var type = values[0].valueType;
			for (var i = 1; i < values.length; i++)
				if (type !== values[i].valueType) {
					type = ReferencedValue.VALUE_TYPE.UNKNOWN;
					break;
				}
			if (type === ReferencedValue.VALUE_TYPE.NULL)
				value = ValueNull.instance;
			else if (type === ReferencedValue.VALUE_TYPE.UNDEFINED)
				value = ValueUndefined.instance;
			else
				value = new ReferencedValue(type);
		}
		return new ValueDistribution([{ condition: this.state.condition, value: value }]);
	} else {
		//
		// binary operator
		//
		var isShortCircuitOperator = ast.token.text === '&&' || ast.token.text === '||';
		var opIsAssignment = Alkaline.JSInfo.opIsAssignment(ast.token.text);
		if (!isShortCircuitOperator && !Alkaline.JSInfo.returnValueIsUsed(ast) && !opIsAssignment) {
			// when op result is not used:
			// A op B --> (A, B)
			var newExpr = this.builder.EXPR(ast.children);
			this.builder.replaceTree(ast, newExpr);
			this.ops++;
			this.process(newExpr);
			return;
		}
		var actualOp = ast.token.text;
		if (ast.token.text !== '=') {
			if (opIsAssignment)
				actualOp = actualOp.substring(0, actualOp.length-1);
			if (Alkaline.JSInfo.binaryNumberOperatorsHash[actualOp]) {
				ast.children[0].coercedTo =
				ast.children[1].coercedTo =
					Alkaline.Processing.ReferencedValue.VALUE_TYPE.NUMBER;
				ast.children[0].coercedBy =
				ast.children[1].coercedBy =
					ast;
			}
			var lhs = this.process(ast.children[0]);
		}
		if (isShortCircuitOperator) {
			var firstPart = this.getTruthiness(lhs);
			if (firstPart !== null) {
				var reqTrue = ast.token.text === '&&';
				var result = firstPart === reqTrue ? ast.children[1] : ast.children[0];
				this.builder.replaceTree(ast, result);
				this.ops++;
				return this.process(result);
			}
			this.state.pushCondition(new Condition(this.state.condition));
			this.poisonLocals(ast.children[0]);
		}
		var rhs = this.process(ast.children[1]);
		if (isShortCircuitOperator) {
			this.unpoisonLocals();
			this.state.popCondition();
		}
		var store = null;
		if (opIsAssignment)
			store = this.getReference(ast.children[0]);
		if (this.arguments.O1) {
			if (lhs) {
				var op = new Operation(ast.token.text, this.state.condition, lhs, rhs);
				result = op.getResult();
			} else
				result = rhs;
			if (store)
				store.setValue(this.state.condition, result);
			else
				this.replaceTreeWithResultIfStatic(ast, result, lhs, rhs);
		}
		return result;
	}
}

this.processPostOp = function(ast) {
	var arg = this.process(ast.children[0]);
	var value = new ReferencedValue(ReferencedValue.VALUE_TYPE.NUMBER, null);
	return new ValueDistribution([{ condition: this.state.condition, value: value }]);
}

this.processReturn = function(ast) {
	if (ast.children && ast.children.length)
		this.process(ast.children[0]);
}

this.processSELECT = function(ast) {
	this.process(ast.children[0]);
	this.process(ast.children[1]);
	var value = new ReferencedValue(ReferencedValue.VALUE_TYPE.UNKNOWN);
	return new ValueDistribution([{ condition: this.state.condition, value: value }]);
}

this.processSwitch = function(ast) {
	var item = this.process(ast.children[0]);
	while (ast.children.length > 1) {
		var last = ast.children.last();
		var block = last.children[last.token.type === TOKEN.Case ? 1 : 0];
		if (block.children && block.children.length)
			if (block.children[0].token.type !== TOKEN.Break || block.children[0].targetInstruction !== ast)
				break;
		this.builder.removeTree(ast.children[last.childIndex]);
		this.ops++;
	}
	if (ast.children.length === 1) {
		this.builder.replaceTree(ast, ast.children[0]);
		this.ops++;
		return;
	}
	// compare using ===
	for (var i = 1; i < ast.children.length; i++)
		this.process(ast.children[i]);
}

this.processThrow = function(ast) {
	var throwable = this.process(ast.children[0]);
	// TODO: manage throw, mark next instructions as invalid
}

this.processTry = function(ast) {
	while (ast.children.length > 1) {
		var child = ast.children[ast.children.length-1].children;
		child = child[child.length-1];
		if (!child.children || !child.children.length)
			this.builder.removeTree(ast.children[ast.children.length-1]);
		else
			break;
	}
	if (ast.children.length === 1) {
		// we removed everything and are left with only "try { ... }"
		ast.parent.replaceChildren(ast.childIndex, ast.childIndex, ast.children[0]);
		this.ops++;
		return this.process(ast.children[0]);
	}
	for (var i = 0; i < ast.children.length; i++)
		this.process(ast.children[i]);
}

this.processTypeOf = function(ast) {
	var arg = this.process(ast.children[0]);
	var value = new ReferencedValue(ReferencedValue.VALUE_TYPE.STRING, null);
	return new ValueDistribution([{ condition: this.state.condition, value: value }]);
}

this.processUnaryOperator = function(ast) {
	if (ast.token.text.length === 1 && !Alkaline.JSInfo.returnValueIsUsed(ast)) {
		this.builder.replaceTree(ast, ast.children[0]);
		this.ops++;
		this.process(ast.children[0]);
		return;
	}
	if (ast.token.text === "!") {
		ast.children[0].coercedTo = ReferencedValue.VALUE_TYPE.BOOLEAN;
		ast.children[0].coercedBy = ast;
		if (ast.children[0].token.text === ast.token.text && ast.children[0].token.type === TOKEN.UnaryOperator
			&& (ast.coercedTo === ReferencedValue.VALUE_TYPE.BOOLEAN || (ast.children[0].children[0].token.type === TOKEN.Operator && Alkaline.JSInfo.comparisonOperatorsHash[ast.children[0].children[0].token.text]))) {
			// --x is not covered here because that transforms to Number
			// if (!!x) --> if (x)
			// a(!!(x < y)) --> a(x < y)
			this.ops++;
			ast.parent.replaceChildren(ast.childIndex, ast.childIndex, ast.children[0].children[0]);
			ast.children[0].children[0].coercedTo = ast.coercedTo;
			ast.children[0].children[0].coercedBy = ast.coercedBy;
			return this.process(ast.children[0].children[0]);
		}
		if (ast.children[0].token.type === TOKEN.Operator && Alkaline.JSInfo.alwaysComplementaryOperatorsHash[ast.children[0].token.text]) {
			// !(a == b) --> a != b
			this.ops++;
			ast.children[0].token.text = Alkaline.JSInfo.alwaysComplementaryOperatorsComplement[ast.children[0].token.text];
			ast.parent.replaceChildren(ast.childIndex, ast.childIndex, ast.children[0]);
			ast.children[0].coercedTo = ast.coercedTo;
			ast.children[0].coercedBy = ast.coercedBy;
			return this.process(ast.children[0]);
		}
	}
	var inner = this.process(ast.children[0]);
	var result;
	if (this.arguments.O1) {
		var op = new Operation(ast.token.text, this.state.condition, inner);
		result = op.getResult();
		this.replaceTreeWithResultIfStatic(ast, result, inner);
	}
	if (!result) {
		var value;
		switch (ast.token.text) {
			case '!':
				value = new ReferencedValue(ReferencedValue.VALUE_TYPE.BOOLEAN, null);
				break;
			case '~':
				value = new ReferencedValue(ReferencedValue.VALUE_TYPE.NUMBER, null);
				// TODO: the decimal part is always 0
				break;
			case '+':
			case '-':
				value = new ReferencedValue(ReferencedValue.VALUE_TYPE.NUMBER, null);
				break;
			case '++':
			case '--':
				value = new ReferencedValue(ReferencedValue.VALUE_TYPE.NUMBER, null);
				var store = this.getReference(ast.children[0]);
				if (store)
					store.setValue(this.state.condition, value);
				break;
		}
		result = new ValueDistribution({ condition: this.state.condition, value: value });
	}
	return result;
}

this.processVoid = function(ast) {
	this.process(ast.children[0]);
	return new ValueDistribution({ condition: this.state.condition, value: ast.objectValue });
}

this.processWhile = function(ast) {
	ast.children[0].coercedTo = ReferencedValue.VALUE_TYPE.BOOLEAN;
	ast.children[0].coercedBy = ast;
	var loop = this.process(ast.children[0]);
	if (this.getTruthiness(loop) === false) {
		this.builder.replaceTree(ast, ast.children[0]);
		this.ops++;
		return;
	}

	this.state.pushCondition(new Condition(this.state.condition));
	this.process(ast.children[1]);
	this.state.popCondition();
}

this.processYield = function(ast) {
	this.process(ast.children[0]);
	var value = new ReferencedValue(ReferencedValue.VALUE_TYPE.UNKNOWN);
	return new ValueDistribution([{ condition: this.state.condition, value: value }]);
}

this.processFalse = function(ast) {
	if (!Alkaline.JSInfo.returnValueIsUsed(ast)) {
		this.builder.replaceTree(ast, this.builder.BLOCK([]));
		this.ops++;
		return;
	}
	if (ast.coercedBy === ast.parent) {
		var newTree = null;
		switch (ast.coercedTo) {
			case ReferencedValue.VALUE_TYPE.NUMBER:
				newTree = this.builder.Number(0);
				break;
			case ReferencedValue.VALUE_TYPE.STRING:
				newTree = this.builder.String("false");
				break;
		}
		if (newTree !== null) {
			this.builder.replaceTree(ast, newTree);
			this.ops++;
			return this.process(newTree);
		}
	}
	return new ValueDistribution({ condition: this.state.condition, value: ast.objectValue });
}

this.processTrue = function(ast) {
	if (!Alkaline.JSInfo.returnValueIsUsed(ast)) {
		this.builder.replaceTree(ast, this.builder.BLOCK([]));
		this.ops++;
		return;
	}
	if (ast.coercedBy === ast.parent) {
		var newTree = null;
		switch (ast.coercedTo) {
			case ReferencedValue.VALUE_TYPE.NUMBER:
				newTree = this.builder.Number(1);
				break;
			case ReferencedValue.VALUE_TYPE.STRING:
				newTree = this.builder.String("true");
				break;
		}
		if (newTree !== null) {
			this.builder.replaceTree(ast, newTree);
			this.ops++;
			return this.process(newTree);
		}
	}
	return new ValueDistribution({ condition: this.state.condition, value: ast.objectValue });
}

this.processNull = function(ast) {
	if (!Alkaline.JSInfo.returnValueIsUsed(ast)) {
		this.builder.replaceTree(ast, this.builder.BLOCK([]));
		this.ops++;
		return;
	}
	if (ast.coercedBy === ast.parent) {
		var newTree = null;
		switch (ast.coercedTo) {
			case ReferencedValue.VALUE_TYPE.BOOLEAN:
				newTree = this.builder.False();
				break;
			case ReferencedValue.VALUE_TYPE.NUMBER:
				newTree = this.builder.Number(0);
				break;
			case ReferencedValue.VALUE_TYPE.STRING:
				newTree = this.builder.String("null");
				break;
		}
		if (newTree !== null) {
			this.builder.replaceTree(ast, newTree);
			this.ops++;
			return this.process(newTree);
		}
	}
	return new ValueDistribution({ condition: this.state.condition, value: ast.objectValue });
}

this.processNumber = function(ast) {
	if (!Alkaline.JSInfo.returnValueIsUsed(ast)) {
		this.builder.replaceTree(ast, this.builder.BLOCK([]));
		this.ops++;
		return;
	}
	return new ValueDistribution({ condition: this.state.condition, value: ast.objectValue });
}

this.processString = function(ast) {
	var isStrict = false;
	if (ast.childIndex === 0 && ast.parent.token.type === TOKEN.BLOCK && (!ast.parent.parent || ast.parent.parent.token.type === TOKEN.Function) && ast.token.text === '"use strict"')
		isStrict = true;
	if (!isStrict && !Alkaline.JSInfo.returnValueIsUsed(ast)) {
		this.builder.replaceTree(ast, this.builder.BLOCK([]));
		this.ops++;
		return;
	}
	return new ValueDistribution({ condition: this.state.condition, value: ast.objectValue });
}

this.processRegExp = function(ast) {
	var value = new ValueRegExp(ast.rxText, ast.rxFlags);
	return new ValueDistribution({ condition: this.state.condition, value: value });
}

this.processVar =
this.processConst =
this.processLet =
	function(ast) {
		// nothing.
	}

this.getTruthiness = function(valueDistribution) {
	var ret = null;
	for (var i = 0; i < valueDistribution.values.length; i++) {
		var truthiness = null;
		var value = valueDistribution.values[i];
		switch (value.valueType) {
			case ReferencedValue.VALUE_TYPE.ARRAY:
			case ReferencedValue.VALUE_TYPE.OBJECT:
				truthiness = true;
				break;

			case ReferencedValue.VALUE_TYPE.BOOLEAN:
				truthiness = value.value;
				break;

			case ReferencedValue.VALUE_TYPE.NULL:
				truthiness = false;
				break;

			case ReferencedValue.VALUE_TYPE.STRING:
				if (value.value === null)
					truthiness = null;
				else
					truthiness = !!value.value.length;
				break;
				
			case ReferencedValue.VALUE_TYPE.NUMBER:
				if (value.value === null)
					truthiness = null;
				else
					truthiness = !!value.value;
				break;

			default:
				truthiness = null;
		}
		if (truthiness === null)
			return null;
		if (!i)
			ret = truthiness;
		else if (ret !== truthiness)
			return null;	// mixed
	}
	return ret;
}

this.replaceTreeWithResultIfStatic = function(ast, result, lhs, rhs) {
	if (lhs && result.equals(this.state.condition, lhs)) {
		this.builder.replaceTree(ast, ast.children[0]);
		this.ops++;
	} else if (rhs && result.equals(this.state.condition, rhs)) {
		this.builder.replaceTree(ast, ast.children[1]);
		this.ops++;
	} else if (result.values.length === 1 && result.values[0].isSimpleValue()) {
		this.builder.replaceTree(ast, result.values[0].toAST(this.builder));
		this.ops++;
	}
}

this._processNotImplemented = function(ast) {
	this.compileResult.stdWarn('[Optimize] Processing of ' + Alkaline.JSInfo.tokenName[ast.token.type] + ' is not implemented: %o.', ast);
}

for (var i = 0; i < tokenNames.length; i++)
	this['process' + TOKEN[tokenNames[i]]] = this['process' + tokenNames[i]] || (this['process' + tokenNames[i]] = this._processNotImplemented);

});
